package message

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"gitee.com/arjunxw/iothub-south-client/request"
	"gitee.com/arjunxw/iothub-south-client/util"
	mapset "github.com/deckarep/golang-set"
	"io"
	"os"
	"strings"
	"sync"
	"time"
)

type upgradeInHandler struct {
	upgradeHandler IotHubMessageHandler
	upgradeCache   sync.Map
	request        request.Request
	host           string
}

func newUpgradeInHandler(upgradeHandler IotHubMessageHandler, request request.Request, host string) IotHubMessageHandler {
	if upgradeHandler.NotifyType() != NotifyUpgrade {
		panic("this message handler is not an upgrade handler")
	}
	return &upgradeInHandler{upgradeHandler: upgradeHandler, request: request, host: host}
}

func (uih *upgradeInHandler) NotifyType() string {
	return NotifyUpgrade
}

func (uih *upgradeInHandler) Handle(downstream *Downstream) error {
	if downstream.Data == nil {
		return errors.New("upgrade data is nil")
	}

	var err error
	if data, ok := downstream.Data.(*DsUpgradeInData); ok {
		folder := "Firmsoft"
		var info os.FileInfo
		if info, err = os.Stat(folder); err != nil {
			if os.IsNotExist(err) {
				if err = os.Mkdir(folder, os.ModePerm); err != nil {
					return err
				}
			}
		} else if !info.IsDir() {
			if err = os.Mkdir(folder, os.ModePerm); err != nil {
				return err
			}
		}

		// find the upgrade file
		var f *os.File
		if f, err = os.Open(folder); err != nil {
			return err
		}
		defer f.Close()

		// Read all upgrade files in the folder
		var infos []os.FileInfo
		if infos, err = f.Readdir(-1); err != nil {
			return err
		}

		var (
			UpgradeFileNotFound    byte = 1
			UpgradeFileExist       byte = 2
			UpgradeFileDownloading byte = 3
		)

		var flag byte = 0
		if len(infos) == 0 {
			flag = UpgradeFileNotFound
		} else {
			flag = UpgradeFileNotFound
			for _, i := range infos {
				if i.Name() == data.Firmsoft.FirmsoftID {
					flag = UpgradeFileExist
					break
				} else if i.Name() == data.Firmsoft.FirmsoftID+".tmp" {
					flag = UpgradeFileDownloading
					break
				}
			}
		}

		now := time.Now()
		switch flag {
		case UpgradeFileNotFound:
			// Create temp file
			filename := folder + string(os.PathSeparator) + data.Firmsoft.FirmsoftID + ".tmp"
			var file *os.File
			if file, err = os.Create(filename); err != nil {
				return err
			}
			defer file.Close()

			// Read all devices waiting to be upgraded
			sns := make([]string, 0, len(data.SnList))
			for _, sn := range data.SnList {
				sns = append(sns, sn.(string))
			}
			// if set, ok := uih.upgradeCache.Load(data.Firmsoft.FirmsoftID); ok {
			// 	set.(mapset.Set).Each(func(i interface{}) bool {
			// 		sns = append(sns, i.(string))
			// 		return false
			// 	})
			// }

			url := request.DownloadFirmwareUrl(uih.host, downstream.ProductKey, data.Firmsoft.FirmsoftID)
			// Start downloading the upgrade file
			var body = make([]byte, 512)
			if err = util.HttpGet(url, nil, &body); err != nil {
				// Report upgrade status
				if len(sns) > 0 {
					remark := fmt.Sprintf("failed to download the upgrade file, %s", err.Error())
					for _, sn := range sns {
						_ = uih.request.ReportUpgradeProgress(context.Background(),
							downstream.DalID, downstream.ProductKey, sn, data.UpgradeID, "Failure", 0, remark, now)
					}
				}

				// Failed to download the upgrade file
				os.Remove(filename)
				return err
			}

			// Verify file signature
			var sign string
			if data.Firmsoft.SignAlgo == "MD5" {
				sign = util.MD5(bytes.NewReader(body))
			} else {
				sign = util.SHA1(bytes.NewReader(body))
			}

			if sign == "" || sign != strings.ToUpper(data.Firmsoft.Sign) {
				// Report upgrade status
				if len(sns) > 0 {
					remark := "upgrade file signature verification failed"
					for _, sn := range sns {
						_ = uih.request.ReportUpgradeProgress(context.Background(),
							downstream.DalID, downstream.ProductKey, sn, data.UpgradeID, "Failure", 0, remark, now)
					}
				}

				os.Remove(filename)
				return err
			}

			// Write file
			if _, err = io.Copy(file, bytes.NewReader(body)); err != nil {
				// Report upgrade status
				if len(sns) > 0 {
					remark := fmt.Sprintf("failed to write upgrade file, %s", err.Error())
					for _, sn := range sns {
						_ = uih.request.ReportUpgradeProgress(context.Background(),
							downstream.DalID, downstream.ProductKey, sn, data.UpgradeID, "Failure", 0, remark, now)
					}
				}

				os.Remove(filename)
				return err
			}

			// Rename file
			if err = os.Rename(filename, folder+string(os.PathSeparator)+data.Firmsoft.FirmsoftID); err != nil {
				// Report upgrade status
				if len(sns) > 0 {
					remark := fmt.Sprintf("failed to write upgrade file, %s", err.Error())
					for _, sn := range sns {
						_ = uih.request.ReportUpgradeProgress(context.Background(),
							downstream.DalID, downstream.ProductKey, sn, data.UpgradeID, "Failure", 0, remark, now)
					}
				}

				os.Remove(filename)
				return err
			}

			var upgradeFile *os.File
			if upgradeFile, err = os.Open(folder + string(os.PathSeparator) + data.Firmsoft.FirmsoftID); err != nil {
				// Report upgrade status
				if len(sns) > 0 {
					remark := fmt.Sprintf("failed to open the upgrade file, %s", err.Error())
					for _, sn := range sns {
						_ = uih.request.ReportUpgradeProgress(context.Background(),
							downstream.DalID, downstream.ProductKey, sn, data.UpgradeID, "Failure", 0, remark, now)
					}
				}

				os.Remove(filename)
				return err
			}
			defer upgradeFile.Close()

			// Read the device waiting to be upgraded again
			if set, ok := uih.upgradeCache.Load(data.Firmsoft.FirmsoftID); ok {
				set.(mapset.Set).Each(func(i interface{}) bool {
					sns = append(sns, i.(string))
					return false
				})
			}
			// Ready to upgrade
			for _, sn := range sns {
				_ = uih.request.ReportUpgradeProgress(context.Background(),
					downstream.DalID, downstream.ProductKey, sn, data.UpgradeID, "Ready", 0, "ready to upgrade", now)
			}

			upgradeData := DsUpgradeData{
				UpgradeID: data.UpgradeID,
				Firmsoft:  data.Firmsoft,
				SnList:    sns,
				File:      upgradeFile,
			}

			defer uih.upgradeCache.Delete(data.Firmsoft.FirmsoftID)
			for _, sn := range sns {
				ds := *downstream
				ds.Sn = sn
				ds.Data = upgradeData
				uih.upgradeHandler.Handle(&ds)
			}
		case UpgradeFileExist:
			sns := make([]string, 0, len(data.SnList))
			for _, sn := range data.SnList {
				sns = append(sns, sn.(string))
			}
			if set, ok := uih.upgradeCache.Load(data.Firmsoft.FirmsoftID); ok {
				set.(mapset.Set).Each(func(i interface{}) bool {
					sns = append(sns, i.(string))
					return false
				})
			}

			var file *os.File
			filename := folder + string(os.PathSeparator) + data.Firmsoft.FirmsoftID
			if file, err = os.Open(filename); err != nil {
				// Report upgrade status
				if len(sns) > 0 {
					remark := fmt.Sprintf("failed to open the upgrade file, %s", err.Error())
					for _, sn := range sns {
						_ = uih.request.ReportUpgradeProgress(context.Background(),
							downstream.DalID, downstream.ProductKey, sn, data.UpgradeID, "Failure", 0, remark, now)
					}
				}

				os.Remove(filename)
				return err
			}
			defer file.Close()

			// ready to upgrade
			for _, sn := range sns {
				_ = uih.request.ReportUpgradeProgress(context.Background(),
					downstream.DalID, downstream.ProductKey, sn, data.UpgradeID, "Ready", 0, "ready to upgrade", now)
			}

			upgradeData := DsUpgradeData{
				UpgradeID: data.UpgradeID,
				Firmsoft:  data.Firmsoft,
				SnList:    sns,
				File:      file,
			}

			defer uih.upgradeCache.Delete(data.Firmsoft.FirmsoftID)
			for _, sn := range sns {
				ds := *downstream
				ds.Sn = sn
				ds.Data = upgradeData
				uih.upgradeHandler.Handle(&ds)
			}
		case UpgradeFileDownloading:
			// The upgrade file is being downloaded, put the device number in the upgrade list
			set, _ := uih.upgradeCache.LoadOrStore(data.Firmsoft.FirmsoftID, mapset.NewSet())
			if set, ok := set.(mapset.Set); ok {
				for _, sn := range data.SnList {
					set.Add(sn.(string))
				}
			}
		}
	}

	return nil
}
